extern crate docopt;
#[phase(plugin)] extern crate docopt_macros;
+use std::collections::TreeSet;
use std::os;
+use std::io;
+use std::io::fs;
use std::io::process::{Command,InheritFd,ExitStatus,ExitSignal};
use serialize::Encodable;
use docopt::FlagParser;
cargo <command> [<args>...]
cargo -h | --help
cargo -V | --version
+ cargo --list
Options:
-h, --help Display this message
-V, --version Print version info and exit
+ --list List installed commands
-v, --verbose Use verbose output
Some common cargo commands are:
fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult<Option<()>> {
debug!("executing; cmd=cargo; args={}", os::args());
shell.set_verbose(flags.flag_verbose);
+ if flags.flag_list {
+ println!("Installed Commands:");
+ for command in list_commands().iter() {
+ println!(" {}", command);
+ // TODO: it might be helpful to add result of -h to each command.
+ };
+ return Ok(None)
+ }
let mut args = flags.arg_args.clone();
args.insert(0, flags.arg_command.clone());
match flags.arg_command.as_slice() {
let r = cargo::call_main_without_stdin(execute, shell,
["-h".to_string()], false);
cargo::process_executed(r, shell)
- }
+ },
orig_cmd => {
- let cmd = if orig_cmd == "help" {
+ let is_help = orig_cmd == "help";
+ let cmd = if is_help {
flags.arg_args[0].as_slice()
} else {
orig_cmd
};
- let command = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX);
- let mut command = match os::self_exe_path() {
- Some(path) => {
- let p1 = path.join("../lib/cargo").join(command.as_slice());
- let p2 = path.join(command.as_slice());
- if p1.exists() {
- Command::new(p1)
- } else if p2.exists() {
- Command::new(p2)
- } else {
- Command::new(command)
- }
- }
- None => Command::new(command),
- };
- let command = if orig_cmd == "help" {
+ execute_subcommand(cmd, is_help, &flags, shell)
+ }
+ }
+ Ok(None)
+}
+
+fn execute_subcommand(cmd: &str, is_help: bool, flags: &Flags, shell: &mut MultiShell) -> () {
+ match find_command(cmd) {
+ Some(command) => {
+ let mut command = Command::new(command);
+ let command = if is_help {
command.arg("-h")
} else {
command.args(flags.arg_args.as_slice())
let msg = format!("subcommand failed with signal: {}", i);
handle_error(CliError::new(msg, i as uint), shell)
}
- Err(_) => handle_error(CliError::new("No such subcommand", 127),
- shell)
+ Err(io::IoError{kind, ..}) if kind == io::FileNotFound =>
+ handle_error(CliError::new("No such subcommand", 127), shell),
+ Err(err) => handle_error(
+ CliError::new(
+ format!("Subcommand failed to run: {}", err), 127),
+ shell)
+ }
+ },
+ None => handle_error(CliError::new("No such subcommand", 127), shell)
+ }
+}
+
+/// List all runnable commands. find_command should always succeed
+/// if given one of returned command.
+fn list_commands() -> TreeSet<String> {
+ let command_prefix = "cargo-";
+ let mut commands = TreeSet::new();
+ for dir in list_command_directory().iter() {
+ let entries = match fs::readdir(dir) {
+ Ok(entries) => entries,
+ _ => continue
+ };
+ for entry in entries.iter() {
+ let filename = match entry.filename_str() {
+ Some(filename) => filename,
+ _ => continue
+ };
+ if filename.starts_with(command_prefix) &&
+ filename.ends_with(os::consts::EXE_SUFFIX) &&
+ is_executable(entry) {
+ let command = filename.slice(
+ command_prefix.len(),
+ filename.len() - os::consts::EXE_SUFFIX.len());
+ commands.insert(String::from_str(command));
}
}
}
- Ok(None)
+ commands
+}
+
+fn is_executable(path: &Path) -> bool {
+ match fs::stat(path) {
+ Ok(io::FileStat{kind, perm, ..}) =>
+ (kind == io::TypeFile) && perm.contains(io::OtherExecute),
+ _ => false
+ }
+}
+
+/// Get `Command` to run given command.
+fn find_command(cmd: &str) -> Option<Path> {
+ let command_exe = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX);
+ let dirs = list_command_directory();
+ let mut command_paths = dirs.iter().map(|dir| dir.join(command_exe.as_slice()));
+ command_paths.find(|path| path.exists())
+}
+
+/// List candidate locations where subcommands might be installed.
+fn list_command_directory() -> Vec<Path> {
+ let mut dirs = vec![];
+ match os::self_exe_path() {
+ Some(path) => {
+ dirs.push(path.join("../lib/cargo"));
+ dirs.push(path);
+ },
+ None => {}
+ };
+ match std::os::getenv("PATH") {
+ Some(val) => {
+ for dir in os::split_paths(val).iter() {
+ dirs.push(Path::new(dir))
+ }
+ },
+ None => {}
+ };
+ dirs
}
#[deriving(Encodable)]
--- /dev/null
+use cargo::util::{process, ProcessBuilder};
+use hamcrest::{assert_that};
+use std::io;
+use std::io::fs;
+use std::os;
+use support::paths;
+use support::{project, execs, cargo_dir, mkdir_recursive, ProjectBuilder, ResultTest};
+
+fn setup() {
+}
+
+/// Add an empty file with executable flags (and platform-dependent suffix).
+/// TODO: move this to `ProjectBuilder` if other cases using this emerge.
+fn fake_executable(proj: ProjectBuilder, dir: &Path, name: &str) -> ProjectBuilder {
+ let path = proj.root().join(dir).join(format!("{}{}", name, os::consts::EXE_SUFFIX));
+ mkdir_recursive(&Path::new(path.dirname())).assert();
+ fs::File::create(&path).assert();
+ let io::FileStat{perm, ..} = fs::stat(&path).assert();
+ fs::chmod(&path, io::OtherExecute | perm).assert();
+ proj
+}
+
+/// Copy real cargo exeutable just built to specified location, and
+/// prepare to run it.
+fn copied_executable_process(proj: &ProjectBuilder, name: &str, dir: &Path) -> ProcessBuilder {
+ let name = format!("{}{}", name, os::consts::EXE_SUFFIX);
+ let path_src = cargo_dir().join(name.clone());
+ let path_dst = proj.root().join(dir).join(name);
+ mkdir_recursive(&Path::new(path_dst.dirname())).assert();
+ fs::copy(&path_src, &path_dst).assert();
+ process(path_dst)
+ .cwd(proj.root())
+ .env("HOME", Some(paths::home().as_vec()))
+}
+
+test!(list_commands_empty {
+ let proj = project("list-runs");
+ let pr = copied_executable_process(&proj, "cargo", &Path::new("bin")).arg("--list");
+ assert_that(pr, execs()
+ .with_status(0)
+ .with_stdout("Installed Commands:\n"));
+})
+
+test!(list_commands_non_overlapping {
+ // lib/cargo | cargo-3
+ // bin/ | cargo-2
+ // PATH | cargo-1
+ // Check if --list searches all 3 targets.
+ // Also checks that results are in lexicographic order.
+ let proj = project("list-non-overlapping");
+ let proj = fake_executable(proj, &Path::new("lib/cargo"), "cargo-3");
+ let proj = fake_executable(proj, &Path::new("bin"), "cargo-2");
+ let proj = fake_executable(proj, &Path::new("path-test"), "cargo-1");
+ let pr = copied_executable_process(&proj, "cargo", &Path::new("bin")).arg("--list");
+
+ let path_test = proj.root().join("path-test");
+ // On Windows, cargo.exe seems to require some directory (
+ // I don't know which) to run properly.
+ // That's why we append to $PATH here, instead of overwriting.
+ let path = os::getenv_as_bytes("PATH").unwrap();
+ let mut components = os::split_paths(path);
+ components.push(path_test);
+ let path_var = os::join_paths(components.as_slice()).assert();
+ assert_that(
+ pr.env("PATH", Some(path_var.as_slice())),
+ execs()
+ .with_status(0)
+ .with_stdout("Installed Commands:\n 1\n 2\n 3\n"));
+})